use chrono::{DateTime, Local};
use reqwest::{
    header::{AUTHORIZATION, CONTENT_TYPE, USER_AGENT},
    Client, Error as ReqwestError,
};
use std::vec;

#[derive(Clone)]
pub struct Profile {
    mesh_id: String,
    user_id: i32,
}

#[derive(Clone)]
pub struct Lesson {
    id: i32,
    starts_at: String,
    ends_at: String,
    name: String,
    room: String,
    marks: Vec<Mark>,
    homework: Vec<String>,
    web_homework: Vec<WebHomework>,
}

#[derive(Clone)]
pub struct Mark {
    id: i32,
    value: String,
    weight: i32,
    comment: String,
}

#[derive(Clone)]
pub struct MarkPeriod {
    title: String,
    marks: Vec<Mark>,
    average: String,
    result: String,
}

#[derive(Clone)]
pub struct SubjectMarks {
    name: String,
    id: i32,
    average: String,
    periods: Vec<MarkPeriod>,
    year_result: String,
}

#[derive(Clone)]
pub struct WebHomework {
    entry_id: i32,
    material_id: String,
}

#[derive(Clone)]
pub struct Rank {
    date: DateTime<Local>,
    rank: i32,
}

impl Profile {
    pub fn new(mesh_id: String, user_id: i32) -> Profile {
        return Profile { mesh_id, user_id };
    }

    pub fn get_mesh_id(self) -> String {
        return self.mesh_id;
    }

    pub fn get_user_id(self) -> i32 {
        return self.user_id;
    }
}

impl Lesson {
    pub fn new(
        id: i32,
        starts_at: String,
        ends_at: String,
        name: String,
        room: String,
        marks: Vec<Mark>,
        homework: Vec<String>,
        web_homework: Vec<WebHomework>,
    ) -> Lesson {
        return Lesson {
            id,
            starts_at,
            ends_at,
            name,
            room,
            marks,
            homework,
            web_homework,
        };
    }

    pub fn get_id(self) -> i32 {
        return self.id;
    }

    pub fn get_starts_at(self) -> String {
        return self.starts_at;
    }

    pub fn get_ends_at(self) -> String {
        return self.ends_at;
    }

    pub fn get_name(self) -> String {
        return self.name;
    }

    pub fn get_room(self) -> String {
        return self.room;
    }

    pub fn get_marks(self) -> Vec<Mark> {
        return self.marks;
    }

    pub fn get_homework(self) -> Vec<String> {
        return self.homework;
    }

    pub fn get_web_homework(self) -> Vec<WebHomework> {
        return self.web_homework;
    }
}

impl WebHomework {
    pub fn new(entry_id: i32, material_id: String) -> WebHomework {
        return WebHomework {
            entry_id,
            material_id,
        };
    }

    pub fn get_entry_id(self) -> i32 {
        return self.entry_id;
    }

    pub fn get_material_id(self) -> String {
        return self.material_id;
    }

    pub async fn to_link(self, auth: String) -> Result<String, ReqwestError> {
        let client: Client = Client::new();
        let req = client
            .get(format!(
                "https://authedu.mosreg.ru/{0}?homework_entry_id={1}&material_id={2}",
                "api/ej/partners/v1/homeworks/launch",
                self.clone().get_entry_id(),
                self.clone().get_material_id()
            ))
            .header("X-mes-Subsystem", "familyweb")
            .header("X-mes-Role", "student")
            .header(CONTENT_TYPE, "application/json; charset=utf-8")
            .header("Profile-Type", "student")
            .header(USER_AGENT, "dot/oms (https://gitverse.ru/dot/oms)")
            .header(AUTHORIZATION, "Bearer ".to_owned() + &auth)
            .header("Auth-Token", &auth)
            .send();

        match req.await?.error_for_status() {
            Ok(resp) => {
                return resp.text().await;
            }
            Err(err) => {
                return Err(err);
            }
        }
    }
}

impl Mark {
    pub fn new(id: i32, value: String, weight: i32, comment: String) -> Mark {
        return Mark {
            id,
            value,
            weight,
            comment,
        };
    }

    pub fn get_id(self) -> i32 {
        return self.id;
    }

    pub fn get_value(self) -> String {
        return self.value;
    }

    pub fn get_weight(self) -> i32 {
        return self.weight;
    }

    pub fn get_comment(self) -> String {
        return self.comment;
    }
}

impl MarkPeriod {
    pub fn new(title: String, marks: Vec<Mark>, average: String, result: String) -> MarkPeriod {
        return MarkPeriod {
            title,
            marks,
            average,
            result,
        };
    }

    pub fn get_title(self) -> String {
        return self.title;
    }

    pub fn get_marks(self) -> Vec<Mark> {
        return self.marks;
    }

    pub fn get_average(self) -> String {
        return self.average;
    }

    pub fn get_result(self) -> String {
        return self.result;
    }
}

impl SubjectMarks {
    pub fn new(
        name: String,
        id: i32,
        average: String,
        periods: Vec<MarkPeriod>,
        year_result: String,
    ) -> SubjectMarks {
        return SubjectMarks {
            name,
            id,
            average,
            periods,
            year_result,
        };
    }

    pub fn get_title(self) -> String {
        return self.name;
    }

    pub fn get_id(self) -> i32 {
        return self.id;
    }

    pub fn get_average(self) -> String {
        return self.average;
    }

    pub fn get_periods(self) -> Vec<MarkPeriod> {
        return self.periods;
    }

    pub fn get_result(self) -> String {
        return self.year_result;
    }
}

impl Rank {
    pub fn new(date: DateTime<Local>, rank: i32) -> Rank {
        return Rank {
            date, rank
        };
    }

    pub fn get_date(self) -> DateTime<Local> {
        return self.date;
    }

    pub fn get_rank(self) -> i32 {
        return self.rank;
    }
}

/// Get User's Profile via Authentication Token
pub async fn get_profile(auth: String) -> Result<Profile, ReqwestError> {
    let client: Client = Client::new();

    let req = client
        .get(format!(
            "https://authedu.mosreg.ru/{}",
            "api/family/web/v1/profile"
        ))
        .header("X-mes-Subsystem", "familyweb")
        .header(USER_AGENT, "dot/oms (https://gitverse.ru/dot/oms)")
        .header("Auth-Token", &auth)
        .send();

    match req.await?.error_for_status() {
        Ok(resp) => {
            let parsed = json::parse(resp.text().await?.as_str()).unwrap();
            let profile: Profile = Profile {
                mesh_id: parsed["children"][0]["contingent_guid"].to_string(),
                user_id: parsed["children"][0]["id"]
                    .as_i32()
                    .expect("User ID not found."),
            };

            return Ok(profile);
        }
        Err(err) => {
            return Err(err);
        }
    }
}

/// Get User's Schedule with User's Profile, Token and Dates
/// Dates have to be in a format of "yyyy-MM-dd", or API will throw an error
pub async fn get_schedule(
    auth: String,
    profile: Profile,
    start_date: DateTime<Local>,
    end_date: DateTime<Local>,
) -> Result<Vec<Lesson>, ReqwestError> {
    let formatted_start_date = start_date.format("%Y-%m-%d");
    let formatted_end_date = end_date.format("%Y-%m-%d");

    let client: Client = Client::new();

    let req = client
        .get(format!(
            "https://authedu.mosreg.ru/{0}?person_ids={1}&begin_date={2}&end_date={3}&expand=marks,homework",
            "api/eventcalendar/v1/api/events",
            profile.get_mesh_id(),
            formatted_start_date,
            formatted_end_date
        ))
        .header("X-mes-Subsystem", "familyweb")
        .header("X-mes-Role", "student")
        .header(USER_AGENT, "dot/oms (https://gitverse.ru/dot/oms)")
        .header(AUTHORIZATION, auth)
        .send();

    match req.await?.error_for_status() {
        Ok(resp) => {
            let parsed = json::parse(resp.text().await?.as_str()).unwrap();
            let mut lessons: Vec<Lesson> = vec![];

            for lesson in parsed["response"].members() {
                let mut marks: Vec<Mark> = vec![];
                let mut homework: Vec<String> = vec![];
                let mut web_homework: Vec<WebHomework> = vec![];
                if !lesson["marks"].is_null() {
                    for mark in lesson["marks"].members() {
                        marks.push(Mark {
                            id: mark["id"].as_i32().expect("ID is not an int."),
                            value: mark["value"].to_string(),
                            weight: mark["weight"].as_i32().expect("Weight is not an int."),
                            comment: mark["comment"].to_string(),
                        });
                    }
                }
                if !lesson["homework"].is_null() {
                    for homework_entry in lesson["homework"]["descriptions"].members() {
                        homework.push(
                            homework_entry
                                .as_str()
                                .expect("Homework entry doesn't exist.")
                                .to_string(),
                        );
                    }
                    for web_homework_entry in lesson["homework"]["entries"].members() {
                        if web_homework_entry["materials"]
                            != "{\"material_ids\":[],\"materialObj\":[]}"
                        {
                            let materials = json::parse(
                                web_homework_entry["materials"]
                                    .as_str()
                                    .expect("Materials object doesn't exist."),
                            )
                            .unwrap();
                            for material in materials["material_ids"].members() {
                                web_homework.push(WebHomework {
                                    entry_id: web_homework_entry["homework_entry_id"]
                                        .as_i32()
                                        .expect("Homework Entry ID doesn't exist."),
                                    material_id: material.to_string(),
                                });
                            }
                        }
                    }
                }
                lessons.push(Lesson {
                    id: lesson["id"].as_i32().expect("ID is not present."),
                    starts_at: lesson["start_at"]
                        .as_str()
                        .expect("Starting time not present.")
                        .to_string(),
                    ends_at: lesson["finish_at"]
                        .as_str()
                        .expect("Ending time not present.")
                        .to_string(),
                    name: lesson["subject_name"]
                        .as_str()
                        .expect("Subject name not present.")
                        .to_string(),
                    room: lesson["room_number"]
                        .as_str()
                        .expect("Room number not present.")
                        .to_string(),
                    marks: marks.clone(),
                    homework: homework.clone(),
                    web_homework: web_homework.clone(),
                })
            }

            return Ok(lessons);
        }
        Err(err) => {
            return Err(err);
        }
    }
}

/// Get User's Marks for Every Subject
pub async fn get_marks(auth: String, profile: Profile) -> Result<Vec<SubjectMarks>, ReqwestError> {
    let client: Client = Client::new();
    let req = client
        .get(format!(
            "https://authedu.mosreg.ru/{0}?student_id={1}",
            "api/family/web/v1/subject_marks", profile.user_id
        ))
        .header("X-mes-Subsystem", "familyweb")
        .header(USER_AGENT, "dot/oms (https://gitverse.ru/dot/oms)")
        .header("Auth-Token", &auth)
        .send();

    match req.await?.error_for_status() {
        Ok(resp) => {
            let parsed = json::parse(resp.text().await?.as_str()).unwrap();
            let mut result_marks: Vec<SubjectMarks> = vec![];

            for subject in parsed["payload"].members() {
                let mut periods: Vec<MarkPeriod> = vec![];

                if !subject["periods"].is_null() {
                    for period in subject["periods"].members() {
                        let mut marks: Vec<Mark> = vec![];
                        if !period["marks"].is_null() {
                            for period_mark in period["marks"].members() {
                                marks.push(Mark {
                                    id: period_mark["id"].as_i32().expect("ID is not an int."),
                                    value: period_mark["value"].to_string(),
                                    weight: period_mark["weight"]
                                        .as_i32()
                                        .expect("Weight is not an int."),
                                    comment: period_mark["comment"].to_string(),
                                });
                            }
                        }
                        periods.push(MarkPeriod {
                            title: period["title"].to_string(),
                            marks,
                            average: period["value"].to_string(),
                            result: period["fixed_value"].to_string(),
                        })
                    }
                }

                result_marks.push(SubjectMarks {
                    name: subject["subject_name"].to_string(),
                    id: subject["subject_id"]
                        .as_i32()
                        .expect("Subject ID is not an int."),
                    average: subject["average_by_all"].to_string(),
                    periods,
                    year_result: subject["year_mark"].to_string(),
                });
            }

            return Ok(result_marks);
        }
        Err(err) => {
            return Err(err);
        }
    }
}

/// Get User's Rank. Returns 0 in Rank.rank if failed to get the placement.
pub async fn get_rankshort(auth: String, profile: Profile, date: DateTime<Local>) -> Result<Rank, ReqwestError> {
    let formatted_date = date.format("%Y-%m-%d");

    let client: Client = Client::new();
    let req = client
        .get(format!("https://authedu.mosreg.ru/{0}?beginDate={1}&endDate={1}&personId={2}", "api/ej/rating/v1/rank/rankShort", formatted_date, profile.mesh_id))
        .header(USER_AGENT, "dot/oms (https://gitverse.ru/dot/oms)")
        .header("X-mes-Subsystem", "familyweb")
        .header("Auth-Token", &auth)
        .send();

    match req.await?.error_for_status() {
        Ok(resp) => {
            let parsed = json::parse(resp.text().await?.as_str()).unwrap();

            let mut result_rank: i32 = 0;
            for rank in parsed.members() {
                result_rank = rank["rankPlace"].as_i32().expect("Rank (place) is not an int or does not exist.");
            };

            return Ok(Rank { date, rank: result_rank });
        }
        Err(err) => {
            return Err(err);
        }
    }
}

/// Get User's Authentication Token via Old, Valid Authentication Token
pub async fn refresh_token(auth: String) -> Result<String, ReqwestError> {
    let client: Client = Client::new();
    let req = client
        .get(format!("https://authedu.mosreg.ru/{}", "v2/token/refresh"))
        .header(USER_AGENT, "dot/oms (https://gitverse.ru/dot/oms)")
        .header(AUTHORIZATION, "Bearer ".to_owned() + &auth)
        .send();

    match req.await?.error_for_status() {
        Ok(resp) => {
            return resp.text().await;
        }
        Err(err) => {
            return Err(err);
        }
    }
}
